# -*- coding: utf-8 -*-
import time
import random
import inspect

from lettuce import step
from lettuce import world

import gameclock
# from gameclock import DebugGameClock as GameClock
from gameclock import GameClock
from gameclock import NegativeDilatationError


# ------------------------------------------------------------------------------
# sensing methods (capturing what is going on)

def update(*args):
    world.captured.append(('update', world.real_time, world.gameclock.game_time, args)) # this is captured after the work of update to easily compare it afterwards
    world.real_time += max(random.gauss(world.update_elapsed, 2), 0.5 * world.update_elapsed)

def draw(*args):
    world.captured.append(("draw", world.real_time, world.gameclock.game_time, args))
    world.real_time += max(random.gauss(world.draw_elapsed, 0.25 * world.draw_elapsed), 0.5 * world.draw_elapsed)

def get_ticks():
    return world.real_time

# ------------------------------------------------------------------------------
# test helper methods (progress the clock and simulate main loop)

def tick_game_clock():
    world.tick_time = world.real_time
    world.captured.append(('tick', world.real_time, world.gameclock.game_time))
    world.gameclock.tick()

def tick_game_clock_n_times(count, mean_tick_time, deviation, min_time_per_tick=1):
    for i in xrange(count):
        world.real_time += max(random.gauss(mean_tick_time, deviation), min_time_per_tick)
        tick_game_clock()

def tick_game_clock_for_interval(interval, mean_tick_time, deviation, min_tick_time=1):
    end = world.real_time + interval
    while world.real_time < interval:
        world.real_time += max(random.gauss(mean_tick_time, deviation), min_tick_time)
        tick_game_clock()
        
def pause_clock(interval):
    world.real_time += interval

# ------------------------------------------------------------------------------


def get_filtered_capture(name):
    return [c for c in world.captured if c[0] == name]

def cap_str(captured):
    return "\n".join([str(c) for c in captured])

# ------------------------------------------------------------------------------


@step(u'Given a default gameclock')
def a_default_gameclock(step):
    # this are use later in test to compare
    world.update_interval = 20
    world.draw_interval = 0 # as fast as possible

    world.gameclock = GameClock(get_ticks, update, world.update_interval, draw, world.draw_interval)

    # variables for the simulation
    world.real_time = 0.0
    # need to capture both 'what' and 'when' it happend
    world.captured = [] # [(what, when, etc), ...]
    # the time a simulated callback takes to complete
    # may be different for slow and fast hardware
    # this base setup is set up for theoretically 50 fps
    world.update_elapsed = 5
    world.draw_elapsed = 15

@step(u'When an update callback is registered')
def register_update_callback(step):
    world.update_elapsed = 1
    world.draw_elapsed = 1
    pass #world.gameclock.update_callback = update

@step(u'Then the update timer will be scheduled for callback')
def update_callback_is_scheduled(step):
    tick_game_clock() # how to know if a update or a draw should have been called?
    updates = get_filtered_capture('update')
    assert len(updates) == 0, "neither update nor draw should have been called since time did not progress"

    world.real_time += world.update_interval / 2
    tick_game_clock() # how to know if a update or a draw should have been called?
    updates = get_filtered_capture('update')
    assert len(updates) == 0, "second neither update nor draw should have been called since time did not progress"

    world.real_time += world.update_interval / 2
    world.real_time += 1
    tick_game_clock() # how to know if a update or a draw should have been called?
    tick_game_clock() # does not do anything since time has not advanced
    updates = get_filtered_capture('update')
    assert world.gameclock.game_time == updates[0][2], "wrong game time" + str(world.gameclock.game_time)
    assert len(updates) == 1, "update should have been called once"
    assert updates[0][0] == 'update', "update should have been called"


@step(u'When the gameclock is ticked')
def when_the_gameclock_is_ticked(step):
    world.real_time += 25
    tick_game_clock()

@step(u'Then the game time is kept in sync with the real time')
def then_the_game_time_is_kept_in_sync_with_the_real_time(step):
    assert abs(world.gameclock.game_time - world.tick_time) < world.update_interval, "gameclock time '%s' is not in sync with real time '%s' %s" % (world.gameclock.game_time, world.tick_time, cap_str(world.captured))


@step(u'When the gameclock is run on fast hardware')
def when_the_gameclock_is_run_on_fast_hardware(step):
    # fast hardware means time for update is small compared to update interval
    # update_elapsed < update_interval
    world.update_interval = 333.3
    world.update_elapsed = 51.3
    # need to instanciate a new gameglock because the values are passed in through the constructor
    world.gameclock = GameClock(get_ticks, update, world.update_interval, draw, world.draw_interval)

@step(u'Then the update occure in regular intervals')
def then_the_update_occure_in_regular_intervals(step):
    # tick the clock to the time of at least 2 * world.update_interval
    # just tick the clock, the calls are recorded
    run_interval = 20 * world.update_interval
    mean = 10
    tick_game_clock_for_interval(run_interval, mean, 0.5 * mean)

    # analyse and assert the captured calls

    # the correct points in time update should be called
    tick_time = 0.0 #world.update_elapsed #world.update_interval + world.update_elapsed
    # using a tolerance because the ticks may not match exactly with the realtime
    tolerance = 1.0 * world.update_elapsed
    for cap in get_filtered_capture('update'):
        what, realwhen, gamewhen, args = cap
        tick_time += world.update_interval
        if realwhen < tick_time - tolerance or tick_time + tolerance < realwhen:
            msg = "wrong tick time '%s' should be at '%s' \n '%s'" % (realwhen, tick_time, cap_str(get_filtered_capture('update')))
            assert False, msg

        if abs(realwhen - gamewhen) >= world.update_interval: # using tolerance
            msg = "realtime '%s' and gametime '%s' drift away \n %s!" % (realwhen, gamewhen, cap_str(get_filtered_capture('update')))
            assert False, msg


@step(u'When an update callback is called')
def when_an_update_callback_is_called(step):
    mean = 10
    tick_game_clock_n_times(50, mean, 0.5 * mean)

@step(u'Then the update arguments contain dt')
def then_the_update_arguments_contain_dt(step):
    for cap in get_filtered_capture('update'):
        cap_dt =  cap[3][0] # this would cause a index out of range if there is no dt

@step(u'Then the update arguments contain gametime')
def then_the_update_arguments_contain_gametime(step):
    for cap in get_filtered_capture('update'):
        cap_game_time =  cap[3][1] # this would cause a index out of range if there is no dt
        assert cap_game_time == cap[2], "gametime does not match, captured: %s from update: %s" % (cap[2], cap_game_time)
    

@step(u'Then the dt is always the same')
def then_the_dt_is_always_the_same(step):
    for cap in get_filtered_capture('update'):
        cap_dt =  cap[3][0]
        assert cap_dt == world.update_interval, "Wrong udpate interval detected '%s' should be '%s' \n %s" % (cap_dt, world.update_interval, cap_str(get_filtered_capture('update')))


@step(u'Then the sum of the dt should be the game time')
def then_the_sum_of_the_dt_should_be_the_game_time(step):
    update_times = [0]
    for cap in get_filtered_capture('update'):
        cap_dt = cap[3][0]
        update_times.append(update_times[-1] + cap_dt)

    update_times.pop(0)

    idx = 0
    for cap in get_filtered_capture('update'):
        # assert update_times[idx] >= cap[1] - tolerance and cap[1] + tolerance >= update_times[idx] , "realtime '%s' differs from accumulated dt time '%s' \n %s" % (cap[1], update_times[idx], "\n".join([str(x) for x in zip(world.captured, update_times)]))
        name, realtime, gametime, args = cap
        assert update_times[idx] == gametime, "gametime '%s' differs from accumulated dt time '%s' \n %s" % (gametime, update_times[idx], "\n".join([str(x) for x in zip(world.captured, update_times) if x[0][0] == 'update']))
        idx += 1

@step(u'When the gameclock is run on slow hardware')
def when_the_gameclock_is_run_on_slow_hardware(step):
    # slow hardware means time for update is big compared to update interval
    # update_elapsed >= update_interval
    world.update_interval = 200.3
    world.update_elapsed = 333.3
    # need to instanciate a new gameglock because the values are passed in through the constructor
    world.gameclock = GameClock(get_ticks, update, world.update_interval, draw, world.draw_interval)

@step(u'Then the game time is running slower than real time')
def then_the_game_time_is_running_slower_than_real_time(step):
    mean = 10
    tick_game_clock_n_times(100, mean, 0.5 * mean)

    captures = get_filtered_capture('update')
    for idx in range(1, len(captures)):
        prev = captures[idx - 1]
        pname, prealtime, pgametime, pargs = prev

        cap = captures[idx]
        name, realtime, gametime, args = cap

        real_dt = abs(realtime - prealtime)
        game_dt = abs(gametime - pgametime)
        assert real_dt >= game_dt, "game time is not slowing down"

@step(u'When a long time between two ticks occure')
def when_a_long_time_between_two_ticks_occure(step):
    tick_game_clock()
    world.real_time += 11110
    tick_game_clock()
    tick_game_clock()
    world.real_time += 20
    tick_game_clock()
    tick_game_clock()


@step(u'Then an upper limit is used for the time delta')
def then_an_upper_limit_is_used_for_the_time_delta(step):
    captures = get_filtered_capture('update')
    assert len(captures) < 10, "too many updates occured after long time between ticks"

@step(u'When an draw callback is registered')
def when_an_draw_callback_is_registered(step):
    # draw is already registered in 'Given a default gameclock'
    tick_game_clock_n_times(10, 5, 1)

@step(u'Then the draw is called regularly')
def then_the_draw_is_called_regularly(step):
    captures = get_filtered_capture('draw')
    assert len(captures) > 1, "draw should have been called!"

@step(u'When the interpolation factor is calculated')
def when_the_interpolation_factor_is_calculated(step):
    tick_game_clock_n_times(1000, 5, 5)

@step(u'Then the interpolation factor will be in the range 0 to 1 exclusive')
def then_the_interpolation_factor_will_be_in_the_range_0_to_1_exclusive(step):
    captures = get_filtered_capture('draw')
    for cap in captures:
        # ("draw", world.real_time, world.gameclock.game_time, args)
        name, real_time, game_time, args = cap
        interpolation = args[0]
        assert interpolation < 1 and interpolation >= 0, "interpolation value out of range"


@step(u'When real time is not evenly divisible by update interval')
def when_real_time_is_not_evenly_divisible_by_update_interval(step):
    world.update_interval = 333.3
    world.update_elapsed = 51.3
    # need to instanciate a new gameglock because the values are passed in through the constructor
    world.gameclock = GameClock(get_ticks, update, world.update_interval, draw, world.draw_interval)

    tick_game_clock_n_times(1000, 15, 5)

@step(u'Then the interpolation factor will be directly proportional to the point in time between the last update and the next update')
def then_the_interpolation_factor_will_be_directly_proportional_to_the_point_in_time_between_the_last_update_and_the_next_update(step):
    capture = list(world.captured)
    last_interp = -1
    while len(capture):
        cap = capture.pop(0)
        if cap[0] == 'update':
            last_interp = -1
        elif cap[0] == 'draw':
            name, real_time, game_time, args = cap
            interpolation = args[0]
            assert interpolation > last_interp, "interpolation value '%s' should be smaller than the last one '%s' \n %s" % (interpolation, last_interp, cap_str(world.captured))
            assert interpolation < 1.0, "interpolation value should be < 1.0 it was: " + str(interpolation)
            last_interp = interpolation
        else:
            pass # ignore any other entries

# -- end doc tests --

@step(u'When the user looks at the class')
def when_the_user_looks_at_the_class(step):
    pass

@step(u'Then the module documentation is present')
def then_the_module_documentation_is_present(step):
    doc = inspect.getdoc(gameclock)
    assert doc is not None, "the module 'gameclock' should have a doc string"
    assert not (doc.startswith('TODO') or doc.startswith('todo')), "module doc string should not start with TODO!"

@step(u'Then the class documentation is present')
def then_the_class_documentation_is_present(step):
    doc = inspect.getdoc(world.gameclock)
    assert doc is not None, "the class 'GameClock' should have a doc string"

@step(u'Then the methods have doc strings')
def then_the_methods_have_doc_strings(step):
    methods = inspect.getmembers(world.gameclock, inspect.ismethod)
    for methodname, method in methods:
        doc = inspect.getdoc(method)
        assert doc is not None, "method '%s' should have a doc string" % (methodname)
        assert not (doc.startswith('TODO') or doc.startswith('todo')), "doc string of method '%s' should not start with TODO!" % (methodname)

@step(u'Then the method arguments are documented')
def then_the_method_arguments_are_documented(step):
    methods = inspect.getmembers(world.gameclock, inspect.ismethod)
    for methodname, method in methods:
        argspec = inspect.getargspec(method)
        doc = inspect.getdoc(method)
        _check_occurence_in_doc(argspec.args, doc, methodname)

def _check_occurence_in_doc(args, doc, name):
        if args is not None:
            for arg in args:
                if arg == 'self':
                    continue
                assert doc is not None, "method '%s' should have a doc string!" % (name)
                assert arg in doc, "Argument '%s' should be documented in method '%s'" % (arg, name)

# -- end doc tests --

@step(u'When maximum fps is 50')
def when_maximum_fps_is_50(step):
    world.draw_interval = 20 # [ms] (because get_ticks is in [ms]) == 50 fps
    world.update_interval = 50
    world.update_elapsed = 0.02
    world.draw_elapsed = 0.5
    world.gameclock = GameClock(get_ticks, update, world.update_interval, draw, world.draw_interval)
    tick_game_clock_n_times(1000, 0, 5)


@step(u'Then the draw is called no more than 50 times per second')
def then_the_draw_is_called_no_more_than_50_times_per_second(step):
    world.captured = world.captured[10:] # remove the first few entries since they behave differently
    draws = get_filtered_capture('draw')
    updates = get_filtered_capture('update')
    ticks = get_filtered_capture('tick')
    real_time = 0.001 * world.captured[-1][1] # need real time in seconds
    fps = len(draws) / real_time
    ups = len(updates) / real_time
    frames = len(ticks) / real_time
    tolerance = 1 # fps
    goal_fps = 1000.0 / world.draw_interval
    # print cap_str(world.captured)
    # print world.captured[-1], '?????? time: ', time, 'ups: ', ups, '  fps: ', fps, '  frames: ', frames, 'goal_fps: ', goal_fps
    assert goal_fps - tolerance <= fps <= goal_fps + tolerance, "expected fps: {0} actual: fps: {1}".format(goal_fps, fps)

@step(u'When maximum fps is given on slow hardware')
def when_maximum_fps_is_given_on_slow_hardware(step):
    world.draw_interval = 1 # [ms]
    world.update_interval = 20
    world.update_elapsed = 100
    world.draw_elapsed = 100
    world.gameclock = GameClock(get_ticks, update, world.update_interval, draw, world.draw_interval)
    tick_game_clock_n_times(1000, 0, 5)
    
@step(u'Then the draw should be scheduled according to realtime')
def then_the_draw_should_be_scheduled_according_to_realtime(step):
    world.captured = world.captured[10:] # remove the first few entries since they behave differently
    while world.captured[0][0] != 'tick':
        world.captured.pop(0)
    # filter the different types of entries
    draws = get_filtered_capture('draw')
    updates = get_filtered_capture('update')
    ticks = get_filtered_capture('tick')
    real_time = 0.001 * world.captured[-1][1] # need real time in seconds
    fps = len(draws) / real_time
    ups = len(updates) / real_time
    frames = len(ticks) / real_time
    tolerance = 1 # fps
    goal_fps = 1000.0 / world.draw_interval
    # print cap_str(world.captured)
    # print world.captured[-1], '?????? time: ', time, 'ups: ', ups, '  fps: ', fps, '  frames: ', frames, 'goal_fps: ', goal_fps
    assert round(fps, 1) == round(frames, 1), "fps should at least be the same as frames! ups: {0} fps: {1} frames: {2}".format(ups, fps, frames)



#  time delatation -------------------------------------------------------------

@step(u'When the time dilation is set to a value smaller 1')
def when_the_time_dilation_is_set_to_a_value_smaller_1(step):
    world.gameclock.dilatation = 0.5
    
@step(u'And the time dilation value is bigger 0')
def and_the_time_dilation_value_is_bigger_0(step):
    assert world.gameclock.dilatation > 0
    
@step(u'Then the time will pass slower')
def then_the_time_will_pass_slower(step):
    tick_game_clock_n_times(1000, 2, 1)
    assert world.real_time >= 1.0 / world.gameclock.dilatation * world.gameclock.game_time, "game time should progress slower than realtime"
    
@step(u'And the draw rate is should still be heigh')
def and_the_draw_rate_is_should_still_be_heigh(step):
    draws = get_filtered_capture('draw')
    real_time = 0.001 * world.captured[-1][1] # need real time in seconds
    fps = len(draws) / real_time
    assert fps > 50, "fps should be heigh enough"
    
@step(u'When the time dilation is set to a value bigger 1')
def when_the_time_dilation_is_set_to_a_value_bigger_1(step):
    world.gameclock.dilatation = 1.5
    
@step(u'Then the time will pass faster')
def then_the_time_will_pass_faster(step):
    tick_game_clock_n_times(1000, 2, 1)
    assert world.real_time <= world.gameclock.dilatation * world.gameclock.game_time, "game time should progress faster than realtime"
    
@step(u'And the draw rate will stay as heigh as possible')
def and_the_draw_rate_will_stay_as_heigh_as_possible(step):
    draws = get_filtered_capture('draw')
    real_time = 0.001 * world.captured[-1][1] # need real time in seconds
    fps = len(draws) / real_time
    assert fps > 30, "fps should be heigh enough"

@step(u'When the time dilation is set to 0')
def when_the_time_dilation_is_set_to_0(step):
    world.gameclock.dilatation = 0
    
@step(u'Then the gametime stops passing by')
def then_the_gametime_stops_passing_by(step):
    tick_game_clock_n_times(1000, 2, 1)
    assert world.gameclock.game_time == 0, "game time should stay at 0 if dilatation is 0"
    
@step(u'When the time dilation is set to a value smaller 0')
def when_the_time_dilation_is_set_to_a_value_smaller_0(step):
    pass
    
# @step(u'Then the gametime decreases instead of increase')
# def then_the_gametime_decreases_instead_of_increase(step):
    # tick_game_clock_n_times(1000, 2, 1)
    # assert world.gameclock.game_time < 0, "game time should be negative: " + str(world.gameclock.game_time)
    # assert world.gameclock.game_time <= -0.5 * world.captured[-1][1], "time should progress slowly backwards"

@step(u'Then the gameclock raises an NegativeDilatationError')
def then_the_gameclock_raises_an_negativedilatationerror(step):
    try:
        world.gameclock.dilatation = -0.5
        assert False, "should have raised 'NegativeDilatationError'"
    except NegativeDilatationError:
        pass
    except:
        assert False, "should not raise any other exception than 'NegativeDilatationError'"
    












